/// <reference path="Client.Data.ts" />

document.body.classList.add("javascript")

const gcInput = document.getElementById('i-wc') as HTMLInputElement
const glenInput = document.getElementById('i-wl') as HTMLInputElement

const genb = document.getElementById('i-b') as HTMLButtonElement

const po = document.getElementById('clist') as HTMLTableSectionElement

namespace Project {
	function resetPrototype(o: object): void {
		Object.setPrototypeOf(Object.getPrototypeOf(o) ?? o, null);
	}

	namespace Private {
		export const NODE_LINKS = Symbol("Project.NodeLink@links");
		export const NODE_LETTER = Symbol("Project.NodeLink@letter");

		export const LINK_PRIVATE = Symbol("Project.NodeLink@data");
	}

	type NodeType = "vowel" | "other" | "consonant"
	export type Type = NodeType;

	export class Node {
		constructor(l: string, t: NodeType = "other") {
			resetPrototype(this);

			this[Private.NODE_LINKS] = [];
			this.letter = l;
			this.type = t;

			Object.defineProperty(this, "length", {
				configurable: false,
				enumerable: true,
				get: (): number => {
					return this[Private.NODE_LINKS].length;
				}
			})
		}

		[Private.NODE_LINKS]: NodeLink[];
		[Private.NODE_LETTER]: string;

		get letter(): string {
			return this[Private.NODE_LETTER];
		}
		set letter(v: string) {
			this[Private.NODE_LETTER] = v;
		}

		type: NodeType;

		declare readonly length: number;

		at(i: number): NodeLink | undefined {
			return this[Private.NODE_LINKS][i];
		}

		link(n: Node, w: number): NodeLink {
			return new NodeLink(this, n, w);
		}
		unlink(n: NodeLink): void {
			n.remove();
		}

		doubleLink(n: Node, w: number): [NodeLink, NodeLink] {
			return [new NodeLink(this, n, w), new NodeLink(n, this, w)];
		}

		nodeLink(n: Node): NodeLink | undefined {
			for (let i = 0; i < this[Private.NODE_LINKS].length; i++) {
				const e = this[Private.NODE_LINKS][i]!;

				if (e.end == n) {
					return e;
				}
			}
			return undefined;
		}

		typedNodes(type: NodeType): NodeLink[] {
			let nlk: NodeLink[] = [];
			this[Private.NODE_LINKS].forEach(e => {
				if (e.end?.type == type) {
					nlk.push(e)
				}
			})

			return nlk;
		}

		next(extype: NodeType | null = null): Node | undefined {
			let links: NodeLink[] = [];

			if (extype !== "consonant") {
				links.push(...this.typedNodes("consonant"));
			}
			if (extype !== "vowel") {
				links.push(...this.typedNodes("vowel"));
			}
			if (extype !== "other") {
				links.push(...this.typedNodes("other"));
			}

			let maxv: number = 0;

			links.forEach(l => {
				maxv += l.weight;
			});

			let rnd = Math.limitRandom(0, maxv);

			let cw = 0;
			let r: Node | undefined = undefined;
			for (let i = 0; i < links.length; i++) {
				const e = links[i];

				if (cw > rnd) {
					break;
				}

				cw += e?.weight ?? 0;
				r = e?.end;
			}

			return r;
		}
	}

	export class NodeLink {
		[Private.LINK_PRIVATE]: {
			node: Node;
			start: Node | null;
			weight: number;
		}

		constructor(owner: Node, to: Node, w: number) {
			resetPrototype(this);

			this[Private.LINK_PRIVATE] = {
				node: to,
				start: owner,
				weight: 0
			}
			owner[Private.NODE_LINKS].push(this);

			Object.defineProperty(this, "begin", {
				configurable: false,
				enumerable: true,
				get: (): Node | null => {
					return this[Private.LINK_PRIVATE].start
				}
			})
			Object.defineProperty(this, "end", {
				configurable: false,
				enumerable: true,
				get: (): Node => {
					return this[Private.LINK_PRIVATE].node
				}
			})

			this.weight = w;
		}

		remove(): void {
			if (!this[Private.LINK_PRIVATE].start) {
				return;
			}

			this[Private.LINK_PRIVATE].start[Private.NODE_LINKS].splice(
				this[Private.LINK_PRIVATE].start[Private.NODE_LINKS].indexOf(this), 1
			);
			this[Private.LINK_PRIVATE].start = null;
		}
		valid(): boolean {
			return !!this[Private.LINK_PRIVATE].start
		}

		declare readonly begin: Node | null;
		declare readonly end: Node;

		get weight(): number {
			return this[Private.LINK_PRIVATE].weight;
		}
		set weight(v: number) {
			this[Private.LINK_PRIVATE].weight = numberToUInt(v);

			if (isNaN(this[Private.LINK_PRIVATE].weight)) {
				this[Private.LINK_PRIVATE].weight = 0
			}
		}
	}

	export interface LettersList {
		'root': Project.Node;
		'А': Project.Node;
		'Б': Project.Node;
		'В': Project.Node;
		'Г': Project.Node;
		'Д': Project.Node;
		'Е': Project.Node;
		'Ё': Project.Node;
		'Ж': Project.Node;
		'З': Project.Node;
		'И': Project.Node;
		'Й': Project.Node;
		'К': Project.Node;
		'Л': Project.Node;
		'М': Project.Node;
		'Н': Project.Node;
		'О': Project.Node;
		'П': Project.Node;
		'Р': Project.Node;
		'С': Project.Node;
		'Т': Project.Node;
		'У': Project.Node;
		'Ф': Project.Node;
		'Х': Project.Node;
		'Ц': Project.Node;
		'Ч': Project.Node;
		'Ш': Project.Node;
		'Щ': Project.Node;
		'Ъ': Project.Node;
		'Ы': Project.Node;
		'Ь': Project.Node;
		'Э': Project.Node;
		'Ю': Project.Node;
		'Я': Project.Node;
	}
	export type LettersCharsKey = keyof LettersList;
	export type LettersSekectorsKey = "vowels" | "static-vowels" | "consonants" | "hissingoffensive";

	export const Letters: {
		readonly root: Project.Node;
		readonly chars: SDT.Registry<Project.Node, Project.LettersCharsKey>;
		readonly selectors: SDT.Registry<Project.LettersCharsKey[], Project.LettersSekectorsKey>;
	} = {
		selectors: new SDT.Registry<Project.LettersCharsKey[], Project.LettersSekectorsKey>(),
		chars: new SDT.Registry<Project.Node, Project.LettersCharsKey>(undefined, (v, v2) => {
			return v.letter === v2.letter;
		}),
		root: new Project.Node('', "other"),
	}
	Object.setPrototypeOf(Letters, null)

	const wva: SDT.RegistryNoteAccess = {
		configurable: false,
		deletable: false,
		enumerable: true,
		readonly: true
	}

	///============///

	Letters.chars.write('root', Letters.root, wva);

	Letters.chars.write('А', new Project.Node('а', "vowel"), wva);
	Letters.chars.write('Е', new Project.Node('е', "vowel"), wva);
	Letters.chars.write('Ё', new Project.Node('ё', "vowel"), wva);
	Letters.chars.write('И', new Project.Node('и', "vowel"), wva);
	Letters.chars.write('О', new Project.Node('о', "vowel"), wva);
	Letters.chars.write('У', new Project.Node('у', "vowel"), wva);
	Letters.chars.write('Ы', new Project.Node('ы', "vowel"), wva);
	Letters.chars.write('Э', new Project.Node('э', "vowel"), wva);
	Letters.chars.write('Ю', new Project.Node('ю', "vowel"), wva);
	Letters.chars.write('Я', new Project.Node('я', "vowel"), wva);

	Letters.chars.write('А', new Project.Node('ъ', "other"), wva);
	Letters.chars.write('А', new Project.Node('ь', "other"), wva);

	Letters.chars.write('Б', new Project.Node('б', "consonant"), wva);
	Letters.chars.write('В', new Project.Node('в', "consonant"), wva);
	Letters.chars.write('Г', new Project.Node('г', "consonant"), wva);
	Letters.chars.write('Д', new Project.Node('д', "consonant"), wva);
	Letters.chars.write('Ж', new Project.Node('ж', "consonant"), wva);
	Letters.chars.write('З', new Project.Node('з', "consonant"), wva);
	Letters.chars.write('Й', new Project.Node('й', "consonant"), wva);
	Letters.chars.write('К', new Project.Node('к', "consonant"), wva);
	Letters.chars.write('Л', new Project.Node('л', "consonant"), wva);
	Letters.chars.write('М', new Project.Node('м', "consonant"), wva);
	Letters.chars.write('Н', new Project.Node('н', "consonant"), wva);
	Letters.chars.write('П', new Project.Node('п', "consonant"), wva);
	Letters.chars.write('Р', new Project.Node('р', "consonant"), wva);
	Letters.chars.write('С', new Project.Node('с', "consonant"), wva);
	Letters.chars.write('Т', new Project.Node('т', "consonant"), wva);
	Letters.chars.write('Ф', new Project.Node('ф', "consonant"), wva);
	Letters.chars.write('Х', new Project.Node('х', "consonant"), wva);
	Letters.chars.write('Ц', new Project.Node('ц', "consonant"), wva);
	Letters.chars.write('Ч', new Project.Node('ч', "consonant"), wva);
	Letters.chars.write('Щ', new Project.Node('ш', "consonant"), wva);
	Letters.chars.write('Ш', new Project.Node('щ', "consonant"), wva);

	///============///

	Letters.selectors.write("vowels", [
		'А', 'Е', 'Ё', 'И', 'О', 'У', 'Э', 'Ю', 'Я'
	])
	Letters.selectors.write("static-vowels", [
		'А', 'Е', 'И', 'О', 'У', 'Э'
	])
	Letters.selectors.write("consonants", [
		'Б', 'В', 'Г', 'Д', 'Ж', 'З', 'К', 'Л', 'М',
		'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х'
	])
	Letters.selectors.write("hissingoffensive", [
		'Ш', 'Щ', 'Ц', 'Ч'
	])

	export type LetterPath = "root" | "~" | `@${LettersSekectorsKey}` | `/${LettersCharsKey}`;

	export function link(src: LetterPath, dest: LetterPath, weight: number = 1, double: boolean = false): void {
		function __parse_path(path: LetterPath): Node[] {
			let r: Node[] = [];
			if (path == '~' || path == 'root') {
				r = [Letters.root];
			}
			else if (path[0] == '/') {
				let cn = Letters.chars.read(path.substring(1));
				r = cn ? [cn] : [];
			}
			else if (path[0] == '@') {
				let cnl = Letters.selectors.read(path.substring(1));
				cnl?.forEach(e => {
					let cn = Letters.chars.read(e);

					if (cn) {
						r.push(cn)
					}
				})
			}

			return r;
		}

		const sarr = __parse_path(src);
		const darr = __parse_path(dest);

		function __link(s: Node[], d: Node[]): void {
			s.forEach(e => {
				d.forEach(et => {
					if (e != et) {
						e.link(et, weight)
					}
				})
			})
		}

		__link(sarr, darr);

		if (double) {
			__link(darr, sarr);
		}
	}

	export function generate(startNode: Project.Node, wordLength: number, maxOneTypeRepeat: number): string {
		let lastType: NodeType = startNode.type;
		let lastTypeRepeat: number = 0;

		maxOneTypeRepeat = Math.max(numberToInt(maxOneTypeRepeat), 1);
		wordLength = numberToUInt(wordLength);

		let r = "";
		let cnode: Project.Node | undefined = startNode;

		for (let i = 0; i <= wordLength && cnode; i++) {
			if (lastType == cnode.type) {
				lastTypeRepeat++;
			}
			else {
				lastType = cnode.type;
				lastTypeRepeat = 0;
			}

			r += cnode.letter;

			cnode = cnode.next(
				lastTypeRepeat >= maxOneTypeRepeat ? lastType : null
			);
		}

		return r;
	}

	const storageRequest = indexedDB.open("SooperADS::WordMeker@index", 1);

	export let appStorage: IDBDatabase | undefined = undefined;

	export interface Word {
		date: Date;
		content: string;
	}
	export interface LocalWord extends Word {
		save: boolean;
	}
	export interface DBWord extends Word {
		uuid: string;
	}

	storageRequest.onsuccess = function () {
		appStorage = storageRequest.result;

		let t = appStorage.transaction("words", "readonly");
		let s = t.objectStore("words");

		let rc = s.openCursor(undefined, "nextunique");

		rc.onerror = function () {
			console.error(rc.error);
		}
		rc.onsuccess = function () {
			const c = rc.result;

			if (c) {
				po.append(
					addWordToTable(new Word(c.value))
				);

				c.continue();
			}
		}
	}
	storageRequest.onerror = function () {
		console.error(storageRequest.error);
	}
	storageRequest.onupgradeneeded = function (e) {
		let db = storageRequest.result;
		switch (e.oldVersion) {
			case 0:
				db.createObjectStore('words', {
					autoIncrement: false,
					keyPath: "uuid"
				});
		}
	}
}

Object.setPrototypeOf(Project, null);

Project.link("@consonants", "@vowels", 1, true);
Project.link("@consonants", "@hissingoffensive", 0.5, true);
Project.link("@hissingoffensive", "/А", 1);
Project.link("@hissingoffensive", "/Е", 1);
Project.link("@hissingoffensive", "/И", 1);
Project.link("@hissingoffensive", "/О", 1);
Project.link("@hissingoffensive", "/У", 1);
Project.link("@hissingoffensive", "/Э", 1);
Project.link("/А", "@hissingoffensive", 0.75);
Project.link("/Е", "@hissingoffensive", 0.75);
Project.link("/И", "@hissingoffensive", 0.75);
Project.link("/О", "@hissingoffensive", 0.75);
Project.link("/У", "@hissingoffensive", 0.75);
Project.link("/Э", "@hissingoffensive", 0.75);
Project.link("@consonants", "/Ь", 0.2);
Project.link("@consonants", "/Ы", 0.75);
Project.link("/Ы", "@consonants", 1);
Project.link("/Ы", "@hissingoffensive", 0.75);
Project.link("/Ь", "@static-vowels", 1);
Project.link("/Й", "@static-vowels", 1);

Project.link('root', "@consonants", 1);
Project.link('root', "@vowels", 1);
Project.link('root', "@hissingoffensive", 1);
Project.link('root', "/Й", 0.7);

function RandomWord(maxLength: number, maxOTR: number = 2): string {
	return Project.generate(Project.Letters.root, Math.intRandom(2, Math.max(maxLength, 2)), maxOTR)
}

///============///

class Word {
	content: string;
	uuid: string;
	date: Date;
	save: boolean;

	constructor(d: Project.DBWord)
	constructor(mlen: number)
	constructor(v: number | Project.DBWord) {
		Object.setPrototypeOf(
			Object.getPrototypeOf(this) ?? this, null
		);

		if (typeof v == "number") {
			this.content = RandomWord(v);
			this.uuid = crypto.randomUUID();
			this.date = new Date();
			this.save = false;
		}
		else {
			this.content = v.content;
			this.uuid = v.uuid;
			this.date = v.date;
			this.save = true;
		}
	}

	dbword(): Project.DBWord {
		return {
			uuid: this.uuid,
			content: this.content,
			date: this.date
		}
	}
}

function addWordToTable(w: Word): HTMLTableRowElement {
	let r = document.createElement("tr");

	const uuid = document.createElement("td");
	uuid.innerHTML = `<code>${w.uuid}</code>`;

	const date = document.createElement("td");
	date.innerHTML = `${w.date.toLocaleDateString()} / ${w.date.toLocaleTimeString()}`;

	const word = document.createElement("td");
	word.innerHTML = `<code>${w.content}</code>`;

	const cbid = `i-cb/${w.uuid}`

	const save = document.createElement("td");
	save.setAttribute("align", "center");
	save.innerHTML = `<input type="checkbox" id="${cbid}" /><label for="${cbid}"></label>`;

	const saveCB = save.getElementsByTagName("input")[0] as HTMLInputElement;
	if (w.save) {
		saveCB.checked = true;
	}

	saveCB.addEventListener("change", () => {
		let ts = Project.appStorage?.transaction("words", "readwrite").objectStore("words");

		if (ts) {
			if (saveCB.checked && !w.save) {
				ts.add(w.dbword());
				w.save = true;
			}
			else if (w.save) {
				ts.delete(w.uuid);
				w.save = false;
			}
		}
	})

	r.append(uuid, date, word, save)
	return r;
}

genb.addEventListener('click', () => {
	if (gcInput.validity.valid && glenInput.validity.valid) {
		for (let i = 0; i < +gcInput.value; i++) {
			po.append(
				addWordToTable(new Word(+glenInput.value))
			);
		}
	}
})

function randomWordTest(count: number, maxLength: number = 8): void {
	console.group("Генерация", count, "случайных слов:")
	for (let i = 0; i < count; i++) {
		let w = RandomWord(maxLength, 2)
		const wo = new Word({
			content: w,
			date: new Date(),
			uuid: crypto.randomUUID()
		});
		wo.save = false;
		po.append(
			addWordToTable(wo)
		);
		console.log(w);
	}
	console.groupEnd()
}

setTimeout(() => {
	randomWordTest(Math.intRandom(2, 7), 15);
}, 100)
